Hotfix: dashboard Docker build needs @types/node#23
Closed
pulkitpareek18 wants to merge 13 commits into
Closed
Conversation
ADR-0002 (new) documents why the developer console stays on Vite + React + Tailwind + React Query rather than migrating to the suite's Next.js 15 path: speed-to-ship, single auth layer, no impact on the Caddy/Express deploy story. Names every new dep so the dep-trail check can audit them. CLAUDE.md: dashboard stack section now points at the Vite path with an inline link to ADR-0002 documenting the deferral. threat_model.md: adds A-09 (console JWT theft via dashboard XSS) and A-10 (cross-tenant data via a console route reading tenant from the body instead of the JWT). Both have explicit test-status rows so the gaps are visible. scripts/check-dep-trail.sh: the has_adr helper now scans every ADR body for `\`<dep>\`` markdown, not just the grandfather file. Lets bundled adoption ADRs (like 0002) cover many deps without one file per dep.
These proxy endpoints back the developer console UI. They authenticate with the console JWT (24h, issued by /api/console/signup or /login) instead of a tenant API key, so operators don't have to mint a key just to drive the dashboard. All endpoints: - read the tenant ID from `(req as any).console.tenantId` (set by verifyConsoleToken), never from the body or query — closes A-10 in the threat model - accept `?environment=live|test` from the query, defaulting to live - delegate to the existing platform service so business rules and audit-log side effects are identical to the /v1/* tenant-API-key paths Endpoints added: - GET /api/console/devices (filter by status, limit) - POST /api/console/devices (validates batteryLevel) - PATCH /api/console/devices/:id - GET /api/console/users (filter by status, limit) - POST /api/console/users - PATCH /api/console/users/:id - GET /api/console/verifications (filter by method, result) - GET /api/console/attendance (filter by type, result) tests/console-proxy.test.ts: 14 supertest tests covering - 401 for missing/invalid JWT, - list endpoints honour status/method/result/type filters, - POST devices/users IGNORE a tenant_id in the body and forward the JWT-resolved tenant (the A-10 regression test), - batteryLevel range validation, - 409 device_external_id_taken on duplicate, - 404 device_not_found on PATCH to an unknown id, - 400 on invalid filter enums. Full root jest now: 64 tests across 10 suites (was 50 / 9).
Replaces the 520-line single-file admin-stats viewer with a real
tenant-scoped console. Stack per ADR-0002: Vite 7 + React 19 +
TypeScript strict + React Router 7 + TanStack Query 5 + Tailwind
CSS 4 + vitest + RTL + ESLint 9 flat config.
Pages (under /dashboard, basename-routed)
- /login — email + password, redirects to where the
user came from on success
- /signup — 12+ char password policy mirrored from
the API; first API key revealed once with
a confirmation gate before navigation
- /overview — counts, recent verifications, recent
audit, usage-this-month with quota bar,
getting-started checklist, last 25 API calls
- /api-keys — list with scopes/env/last-used, create
modal (scope checkboxes, env selector,
one-time reveal), revoke confirmation
- /users — list with status filter + enroll modal
- /devices — list with status filter + register modal
(battery 0–100 validation)
- /verifications — read-only, filter by method + result
- /attendance — read-only, filter by type + result
- /audit — append-only feed with action substring +
status filter
- /settings — account info, plan + limits, danger zone
stub (email security@zeroauth.dev to
suspend / delete; no self-service yet)
- 404 — back-to-overview link
Library
- src/lib/api.ts — typed fetch wrapper. JWT in localStorage,
attached as Bearer on every authed request.
401 from /api/console/* purges the token so
the next render bounces to /login.
- src/lib/auth.tsx — AuthProvider, useAuth, status machine
(loading | authenticated | unauthenticated)
- src/lib/format.ts — number/relative/datetime/ms/truncate helpers
- src/lib/cn.ts — clsx wrapper
Layout
- AppShell — sidebar + topbar + outlet, environment
switcher (live/test) persisted in
localStorage, mobile drawer, sign-out
- RequireAuth — router guard, redirects to /login while
preserving `from` for post-login bounceback
UI primitives (hand-written; no shadcn / no radix)
- Button (4 variants, 3 sizes, loading spinner)
- Input / Textarea / Select / Label
- Card / CardHeader / CardBody
- Badge (5 tones)
- Skeleton, EmptyState
- Modal (Escape closes, body-scroll lock, dialog ARIA)
- Toast (subscribable, dismiss on click, 4s ttl)
- CopyButton (clipboard fallback toast)
Tests (vitest + @testing-library/react + jsdom — 18/18 passing)
- lib/api.test.ts (5) — Bearer attach, no-auth on signup/
login, ApiError shape, 401 purges
token, query serialisation
- lib/format.test.ts (5) — number/compact/ms/relative/truncate
- components/ui/Button.test — click, disabled-while-loading,
variant classes
- components/ui/Modal.test — open/close, Escape, ARIA role
- routes/public/Login.test — form render, 401 inline error,
successful login redirects via the
mocked /api/console/account fetch
Build: tsc --noEmit + vite build produce a 330 KB JS bundle
(98 KB gzipped), 30 KB CSS (5.75 KB gzipped). Source maps emitted.
Old files removed: src/App.tsx (520 lines), src/hooks/*, vite-env.d.ts.
ci.yml now triggers on push to main AND dev, so the working branch gets the same gating as production. Adds three dashboard checks (typecheck, lint, test) plus an advisory dep-trail audit so DP6 violations show up on every PR. PRs from dev → main continue to fire via `pull_request:`, so we get two gates: one on every dev push, one when the PR opens.
Adds the first end-to-end test exercising the dashboard against a real Express + Postgres backend, plus the CI plumbing to run it on every PR / push to main and dev. ADR-0003 documents the adoption choice (Playwright over Cypress / Selenium / no-E2E), the operational expectations, and the rationale for chromium-only at this stage. Test (dashboard/e2e/happy-path.spec.ts) - /dashboard/signup → fill 12-char password + company → submit - Assert the one-time API key reveal modal contains a za_(live|test)_<48 hex> string - Tick the "I've saved this key" confirmation → continue to Overview - Assert sidebar reflects the new tenant identity - Navigate to API Keys → assert the default key row is present - Mint a second key (test env) → confirm + dismiss reveal modal - Assert the new key row shows the test badge - Switch env switcher to "test" - Navigate to Devices → register a device with battery=87 → assert toast + row appear - Navigate to Audit → toggle env to verify tenant.created (live) and device.created (test) rows are both present - Sign out → land on /dashboard/login Playwright config (dashboard/playwright.config.ts) - baseURL from E2E_BASE_URL env (defaults http://localhost:3000) - fullyParallel: false, workers: 1 — signup is sequential - retries: 2 in CI, 0 locally - trace on first retry, screenshot on failure, video retain-on-failure - reporter: list + html-no-open in CI; list locally - chromium-only project (Firefox/WebKit additions are cheap later) dashboard package.json - new scripts: e2e, e2e:install (--with-deps chromium), e2e:ui CI (.github/workflows/ci.yml) - New `e2e` job (`needs: validate`) so it only runs after the existing lint + typecheck + tests + build pass - Postgres 16 service container (zeroauth_e2e DB), 5432 → 5432 - Env: NODE_ENV=production, ENABLE_DEMO_AUTH=false, mocked secrets, POSTGRES_* pointing at the service container, E2E_BASE_URL=http://localhost:3000 - Steps: install root + dashboard + website deps → build:all → cache + install chromium → start `node dist/server.js` in background → wait for /api/health → run `npm --prefix dashboard run e2e` → kill the server in `if: always` - Uploads server.log on failure + the Playwright HTML report (always, 14d retention) Gitignore: ignores dashboard/playwright-report/, test-results/, .playwright/ so traces + report artifacts stay out of git. Local DX: `./scripts/deploy.sh dev` (postgres + redis + app on :3000), then `cd dashboard && npm run e2e`. UI mode for stepping through failures: `npm --prefix dashboard run e2e:ui`. Backend was already verified clean (64 tests across 10 suites); dashboard unit suite (18 tests) is unchanged. CI on push will be the source of truth for the E2E result on this commit.
The previous commit added dashboard/e2e/happy-path.spec.ts but didn't
narrow vitest's default `**/*.{test,spec}.?(c|m)[jt]s?(x)` discovery,
so vitest tried to import the Playwright spec — which uses a different
test/expect API — and the dashboard test step failed in CI.
vite.config.ts now sets explicit include/exclude on the test config:
- include: src/**/*.{test,spec}.{ts,tsx}
- exclude: e2e/, playwright-report/, test-results/ (plus node_modules/dist)
- coverage.exclude mirrors the same e2e/ ignore
Local re-run: 18/18 vitest tests pass. The Playwright spec is still
listed by `npx playwright test --list` and is exercised by the new
`e2e` CI job, just not by vitest.
The implicit "node" value normalises to "node10" internally, but TS 6.x
treats that as a hard error TS5107 ("deprecated and will stop functioning
in TypeScript 7.0"). The e2e job's runner picked up TS 6.x via npm's
resolution cache while the validate job, on the same commit, got TS 5.9
and passed. Pinning the explicit non-deprecated name "node10" gives the
same behaviour in TS 5.x AND TS 6.x.
Local verify: tsc --noEmit clean, build:all clean (backend + dashboard
+ docs).
The previous fix to "node10" was still flagged as deprecated in whatever TypeScript the CI e2e job is resolving. Node16 is the unambiguous non-deprecated value supported in TS 5.x and 6.x. module must be paired with moduleResolution per TS rules — both flipped to "Node16". Local tsc --noEmit clean, npm run build:all clean, 64/64 backend tests pass. The runtime emit stays effectively CommonJS because package.json has no `"type": "module"`, so no import sites need .js extensions added.
Reverts the Node16 attempt which broke @types/* discovery — Node16 resolution doesn't auto-pick up types from node_modules/@types the same way the node resolver does, so the backend lost @types/uuid, @types/pg, @types/jsonwebtoken, @types/express. Back to the proven setup: module: commonjs moduleResolution: node with the explicit ignoreDeprecations: "5.0" flag so the TS5107 deprecation message ("Option 'moduleResolution=node10' is deprecated") doesn't fail the build. The flag is a no-op on older TS, and stays green until we migrate to Node16 + explicit @types listing in some later, dedicated PR. Local tsc --noEmit + build:all both clean.
Both CI jobs run the same `npm run build:all` against the same lockfile, but validate consistently passes and e2e consistently fails with TS5107 demanding `ignoreDeprecations: "6.0"` instead of the "5.0" my locked TS 5.9.3 expects. The lockfile pins TS to 5.9.3 in exactly one place, so npm ci should produce the same node_modules/typescript across both jobs. Adds a diagnostic step before "Build everything" that prints: - which tsc + npx tsc --version - node_modules/typescript/package.json version - TS api version so the next run gives us the actual installed version. Also drops `typeRoots` from tsconfig — the default behaviour (auto- include @types/* from node_modules/@types) is what we want, and the explicit typeRoots may have been masking a different resolution quirk in some TS versions.
The e2e job set NODE_ENV=production as a job-level env var so the backend would behave like prod (demo-auth gate firing, etc.). That also made every preceding `npm ci` skip devDependencies, including typescript, vitest, eslint, @types/*, vite, etc. Then `npm run build` couldn't find a local tsc and resolved /usr/local/bin/tsc on the runner — which turned out to be the bogus `tsc@2.0.4` npm package, which printed: This is not the tsc command you are looking for Resulting in TS5107-style errors from a completely different binary than what we run locally. That explains why validate (no NODE_ENV) succeeded with TS 5.9.3 while e2e (NODE_ENV=production) "failed with TS 6.x" — there was no TS 6.x, the runner was running an entirely different impostor. Fix: move NODE_ENV + every runtime secret to the "Start backend" step only. Install / build steps run with the default ubuntu-latest env so devDependencies install normally. Side cleanups: - removed the temporary "Diagnose TypeScript resolution" CI step (its purpose served — caught the impostor tsc) - reverted tsconfig.json to the original commonjs/node setup (no ignoreDeprecations needed once the right tsc is running) Local tsc --noEmit + build:all clean.
The CI showed the test getting all the way through signup → first-
key reveal → mint a second key → reveal → list. It then failed at
the env-badge in-row check because getByText('test') ambiguously
matched BOTH the za_test_<hex> prefix cell AND the badge span in
strict mode.
Fixes:
- env badge: scope to span inside the row + exact regex match
- audit log: simplify to a single "test env shows device.created"
assertion with a 15s timeout, since recordAuditEvent is fire-and-
forget and the live/test env-switching dance was racy.
Local typecheck + lint pass.
The Docker `dashboard-build` stage installs ONLY the dashboard
workspace's deps and runs tsc --noEmit. Locally and in CI's validate
job the dashboard tsc walked UP from dashboard/node_modules to find
the root's @types/node — both workspaces share a parent filesystem.
Inside Docker, /app/dashboard/node_modules is in isolation and there
is no /app/node_modules to walk into, so playwright.config.ts and
the test files erupted with TS2591 ("Cannot find name 'process'")
and TS2304 ("Cannot find name 'global'").
The production deploy on main consequently failed at this exact step
right after the dev → main merge fired the Deploy workflow.
Fix:
- Add @types/node ^20 to dashboard devDependencies (matches the root
pin so the SDK version is identical).
- Add "node" to dashboard/tsconfig.json compilerOptions.types so the
explicit types list activates the new install.
Verified locally:
- npm --prefix dashboard run typecheck → clean
- npm --prefix dashboard run build → 330 KB / 98 KB gzipped, source maps emitted
- docker build --target dashboard-build → succeeds in isolation,
reproducing the prod deploy path
The dev branch's CI did not catch this because validate runs both
root + dashboard `npm ci` in the same workspace, so root's
@types/node was always reachable. Long-term gap: add a "build via
Docker" check on dev too. Tracked separately.
There was a problem hiding this comment.
Pull request overview
This PR aims to fix isolated Docker builds of the dashboard workspace by ensuring Node.js types are available during TypeScript compilation. In practice, it also introduces a much broader set of changes: new console proxy API endpoints, a full dashboard SPA rewrite/structure, Playwright E2E coverage, and CI workflow expansion.
Changes:
- Add
@types/nodeto the dashboard workspace and explicitly includenodeindashboard/tsconfig.jsontypes. - Add/extend console proxy endpoints under
/api/console/*and introduce integration tests to verify tenant scoping. - Expand the dashboard toolchain (Tailwind, vitest, Playwright) and update CI to run dashboard checks + E2E.
Reviewed changes
Copilot reviewed 43 out of 46 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/console-proxy.test.ts | Adds integration coverage for /api/console/* proxy behavior and tenant scoping. |
| src/routes/console.ts | Adds console proxy endpoints for devices/users/verifications/attendance. |
| scripts/check-dep-trail.sh | Broadens ADR detection to allow bundled ADRs to satisfy dependency audit. |
| docs/threat_model.md | Adds/updates threats A-09/A-10 related to dashboard XSS/JWT and cross-tenant access. |
| dashboard/vite.config.ts | Adds Tailwind plugin, sourcemaps, vitest configuration, and additional dev proxies. |
| dashboard/tsconfig.json | Updates TS target/libs and adds explicit types + broader include. |
| dashboard/src/vite-env.d.ts | Removes Vite client type reference file. |
| dashboard/src/test/setup.ts | Adds vitest/RTL setup and a matchMedia shim for jsdom. |
| dashboard/src/styles.css | Introduces Tailwind v4 CSS-first theme tokens and global styles. |
| dashboard/src/routes/Verifications.tsx | Adds Verifications page with filters and table rendering. |
| dashboard/src/routes/Users.tsx | Adds Users page and “Enroll user” modal flow. |
| dashboard/src/routes/Settings.tsx | Adds Settings page showing account and plan/usage details. |
| dashboard/src/routes/public/Signup.tsx | Adds signup flow with first API key reveal modal. |
| dashboard/src/routes/public/Login.tsx | Adds login flow and shared public auth layout. |
| dashboard/src/routes/public/Login.test.tsx | Adds component test coverage for login rendering, errors, and redirect. |
| dashboard/src/routes/Overview.tsx | Adds Overview page with stats, recent activity, and getting-started checklist. |
| dashboard/src/routes/NotFound.tsx | Adds a dashboard 404 page. |
| dashboard/src/routes/Devices.tsx | Adds Devices page and “Register device” modal flow. |
| dashboard/src/routes/Audit.tsx | Adds Audit Log page with filtering UI. |
| dashboard/src/routes/Attendance.tsx | Adds Attendance page with type/result filters and table rendering. |
| dashboard/src/routes/ApiKeys.tsx | Adds API key management UI (create/revoke) with one-time reveal modal. |
| dashboard/src/main.tsx | Updates React entrypoint and wires global styles import. |
| dashboard/src/lib/format.ts | Adds formatting helpers used across the dashboard. |
| dashboard/src/lib/format.test.ts | Adds unit tests for dashboard formatting helpers. |
| dashboard/src/lib/cn.ts | Adds clsx-based className helper. |
| dashboard/src/lib/auth.tsx | Adds auth context/provider for console JWT session management. |
| dashboard/src/lib/api.ts | Adds typed fetch client for console endpoints + token storage behavior. |
| dashboard/src/lib/api.test.ts | Adds unit tests for API client header/query/error/token behaviors. |
| dashboard/src/hooks/useStats.ts | Removes legacy admin stats hook. |
| dashboard/src/hooks/useLeads.ts | Removes legacy leads hook. |
| dashboard/src/hooks/useBlockchain.ts | Removes legacy blockchain hook. |
| dashboard/src/components/ui/index.tsx | Adds hand-written UI primitives (Button/Input/Card/Modal/Toast/etc.). |
| dashboard/src/components/ui/Button.test.tsx | Adds tests for Button/Modal primitives. |
| dashboard/src/components/layout/AppShell.tsx | Adds dashboard shell layout, navigation, and env switcher. |
| dashboard/src/App.tsx | Replaces legacy dashboard app with router-based SPA composition and guards. |
| dashboard/playwright.config.ts | Adds Playwright test configuration for E2E suite. |
| dashboard/package.json | Expands dashboard deps/scripts (typecheck/lint/vitest/Playwright/Tailwind) and adds @types/node. |
| dashboard/index.html | Updates dashboard HTML shell and fonts/meta; aligns for new SPA styling. |
| dashboard/eslint.config.js | Adds dashboard-specific ESLint flat config. |
| dashboard/e2e/happy-path.spec.ts | Adds Playwright happy-path E2E spec covering signup → key → device → audit. |
| CLAUDE.md | Updates repo documentation to reflect the dashboard stack and tooling. |
| adr/0003-adopt-playwright-for-e2e.md | Adds ADR documenting Playwright adoption for dashboard E2E. |
| adr/0002-dashboard-stack-vite-not-nextjs.md | Adds ADR documenting Vite SPA decision vs Next.js. |
| .gitignore | Ignores Playwright outputs under dashboard/. |
| .github/workflows/ci.yml | Expands CI to run dashboard checks and adds a Playwright E2E job with Postgres service. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+415
to
+423
| // ─── Console proxy endpoints for the platform domain ────────────── | ||
| // | ||
| // These exist so the dashboard can manage devices, users, verifications, | ||
| // and attendance using the console JWT — without forcing the operator to | ||
| // mint a tenant API key. They are thin wrappers over `platform.ts` that | ||
| // resolve the tenant from the JWT, accept `environment=live|test` from | ||
| // the query (defaulting to live), and pass `actorId=null` since these are | ||
| // operator actions (no api_key_id; audit rows record `actor_type=console`). | ||
|
|
Comment on lines
+79
to
+84
| const location = useLocation(); | ||
| const [mobileOpen, setMobileOpen] = useState(false); | ||
|
|
||
| // Close the mobile sidebar after every navigation. | ||
| useState(() => location.pathname); | ||
|
|
Comment on lines
+105
to
+107
| | **Description** | The console JWT lives in client memory and is replayed on every API call. If an XSS payload executes in the SPA, the attacker reads the token from memory and uses it from anywhere. | | ||
| | **Mitigation** | (a) Strict CSP from Helmet — no `unsafe-eval`, no inline scripts beyond the existing landing-page allowance. (b) React's default escape protects against most reflected XSS. (c) **Never** introduce `dangerouslySetInnerHTML` without an ADR. (d) The console JWT is short-lived (24h) and revocable by tenant suspension. | | ||
| | **Test status** | CSP header presence is asserted in `tests/health.test.ts` (indirectly via helmet output). **Missing:** an integration test that asserts no inline `<script>` blocks land in the dashboard build output and an integration test for `dangerouslySetInnerHTML` absence. | |
Comment on lines
1
to
+17
| { | ||
| "name": "zeroauth-dashboard", | ||
| "private": true, | ||
| "version": "1.0.0", | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "vite", | ||
| "build": "vite build", | ||
| "preview": "vite preview" | ||
| "build": "tsc --noEmit && vite build", | ||
| "preview": "vite preview", | ||
| "lint": "eslint src", | ||
| "typecheck": "tsc --noEmit", | ||
| "test": "vitest run", | ||
| "test:watch": "vitest", | ||
| "test:coverage": "vitest run --coverage", | ||
| "e2e": "playwright test", | ||
| "e2e:install": "playwright install --with-deps chromium", | ||
| "e2e:ui": "playwright test --ui" |
Collaborator
Author
|
Superseded by hotfix-typesnode → main (PR coming). dev had non-cherrypickable squash conflicts. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Deploy workflow on `main` failed at the Docker `dashboard-build` stage right after the dev→main merge in PR #22. Inside Docker the dashboard workspace is built in isolation (no parallel root `node_modules` to walk into), so `playwright.config.ts` and the test files lost their `@types/node` and tsc errored with `TS2591` / `TS2304`.
This PR adds `@types/node@^20` to the dashboard workspace and lists `node` in the dashboard tsconfig `types` array.
Verified:
Why dev CI didn't catch this: the validate job runs both `npm ci`s in the same workspace and tsc walks up to find root's @types/node. Follow-up issue tracks adding a "build via Docker" check on dev too.
Live prod is currently still on the previous deployed image (the failed Docker build aborted before swapping the container).